#! /usr/bin/perl

use Carp;
use File::Basename;
use Getopt::Std;
use Net::CIDR ':all';
use POSIX;
use Proc::PID::File;
use Socket;
use strict;

our $VERSION = '0.3';

my $banFile = '/net/abuse-ip/ip.txt';
my $logFile = '/var/log/auth.info';
my $pidFile = '/var/run/sshdGuard-server.pid';

my @whiteBlock;			# whitelist block
my $whiteFile = '/net/abuse-ip/white_ip_list';

my $tolerationMaxTimes;		# this will be calculated automatilly
my %tolerations = (10 => 5, 600 => 8, 3600 => 16);

main();

sub banBadGuy {
    my $ip = shift;

    # check whitelist
    if (cidrlookup $ip, @whiteBlock) {
	print "* ${ip} in whiteBlock\n";
	return;
    }

    # print to stdout
    printf "%s %s\n", (scalar localtime), $ip;

    # write to file
    open B, '>>', ${banFile} or carp $!;
    printf(B "%d %s\n", time, $ip);
    close B;
}

sub createPidFile {
    croak 'Already running' if Proc::PID::File->running(dir => dirname($pidFile), name => basename($pidFile));
}

sub loadWhiteBlock {
    if (open I, $whiteFile) {
	while (<I>) {
	    chomp;
	    @whiteBlock = cidradd $_, @whiteBlock;
	}
	close I;
    }
}

sub loadOptions {
    my %opts;

    getopts 'b:l:p:w:', \%opts;

    $banFile = $opts{b} if defined $opts{b};
    $logFile = $opts{l} if defined $opts{l};
    $pidFile = $opts{p} if defined $opts{p};
    $whiteFile = $opts{w} if defined $opts{w};
}

my %hashError;

sub loginFailed {
    my $ip = shift;

    unless ($ip =~ /^(\d+\.){3}\d+$/) {
	$ip = inet_ntoa scalar gethostbyname $ip;
    }

    print "* Login failed from ${ip}\n";

    my $now = time;
    if (exists $hashError{$ip}) {
	# push the latest time
	unshift @{$hashError{$ip}}, $now;
	pop @{$hashError{$ip}} if (scalar @{$hashError{$ip}} > $tolerationMaxTimes);

	while (my ($tolerationPeriod, $tolerationTimes) = each %tolerations) {
	    if (exists $hashError{$ip}->[$tolerationTimes - 1] &&
		    $now - $hashError{$ip}->[$tolerationTimes - 1] < $tolerationPeriod) {
		banBadGuy $ip;
	    }
	}
    } else {
	$hashError{$ip} = [$now];
    }
}

sub main {
    loadOptions();
    createPidFile();
    loadWhiteBlock();

    # calculate tolerationMaxTimes
    $tolerationMaxTimes = (sort values %tolerations)[0];

    # Open the log file and seek to end
    open L, '-|', "tail -F ${logFile}" or croak $!;
    seek L, 0, SEEK_END;

    readLog(\*L);

    # Theoretically, it won't reach here
    close L;
    croak 'No way to reach here';
}

sub readLog {
    my $fh = shift;

    while (<$fh>) {
	chomp;

	if (/ Invalid user \S+ from (\S+)$/ ||
		/ error: PAM: authentication error for \S+ from (\S+)$/ ||
		/ Failed password for \S+ from (\S+) port \d+ ssh2/) {
	    my $ip = $1;

	    loginFailed $ip;
	}
    }
}
